Transpiler: 트랜스파일이 가능한 TSC를 왜 타입체커로만 쓸까?

트랜스파일(Transpile)은 트랜스폼(transform)과 컴파일(compile)의 합성어다. 말 그대로 “같은 수준의 언어끼리 문법을 변환하는 것”을 의미한다. 즉, 고급 언어 → 저급 언어로 바꾸는 일반적인 컴파일(예: C → 어셈블리)과 달리, 같은 계층의 언어(예: JS → JS) 사이에서 변환이 일어나는 것을 말한다.

우리는 React로 Typescript로 등등 다양한 언어와 버전으로 코드를 작성한다. Javascript도 ESNEXT처럼 신 버전들을 종종 사용하곤 한다. 하지만 브라우저는 ESNEXT를 알지도 못하고, TS나 React를 알지 못한다 브라우저는 오직 HTML,CSS, 안정화된 버전의 Javascript만 해석할 수 있다. 물론 웹어셈블리도 등장했지만..

프론트엔드에서 트랜스파일은 보통 브라우저가 이해하지 못하는 “미래 문법”을 현재 브라우저가 이해할 수 있는 “과거 문법”으로 바꿔주는 과정이다 물론 JS의 수퍼셋인 TS를 JS로 변환해주는 것도 있다.

Transpile 가능한 tsc를 타입체커 용도로만 쓰는 이유

Babel가장 널리쓰이는 트랜스파일러이자 사실상의 표준이다. 다양한 플러그인이 존재하기 때문에 React의 JSX, 최신 ECMAScript 문법, 심지어 TypeScript까지 모두 Babel이 처리할 수 있다. 하지만 타입체킹은 하지 않는다.


TypeScript의 컴파일러 tsc 역시 트랜스파일러다. 하지만 Babel과 가장 달리 tsc는 두 가지 일을 한다.
Babel이 단순히 문법을 바꾸는 “변환기”라면, tsc타입을 해석하고 검증 + 변환까지 해준다.
하지만 우리는 보통 --noEmit 명령어를 통해 타입체킹만 할 뿐, 트랜스파일러로 쓰진 않는다.

왜 그럴까?

TypeScript 컴파일러(tsc)는 타입을 제거하고 자바스크립트 코드를 생성하는 도구이지만, 이 출력물이 브라우저에서 항상 바로 실행될 수 있는 것은 아니다. 실행 가능 여부는 크게 TSConfig의 target 설정에 따라 달라진다. tsc는 타입 제거와 기본적인 다운레벨링까지만 수행하며, 최신 자바스크립트 기능을 완전하게 ES5나 오래된 환경으로 변환하는 도구가 아니다.

예를 들어 TSConfig에서 target: "ES2017" 같은 설정을 사용했다면, 브라우저가 ES2017 기능을 모두 지원하는 경우 해당 결과물은 문제 없이 실행된다. 이 경우 tsc 출력물은 거의 “현재 자바스크립트 코드”이기 때문에 브라우저가 이해할 수 있는 문법 범위만 맞으면 특별한 처리가 필요 없다. 실제로 많은 모던 브라우저 환경에서는 tsc 출력물이 그대로 동작하는 경우가 많다.

하지만 브라우저 호환성을 넓게 고려할 때 tsc만으로는 부족한 경우가 훨씬 많다. 예를 들어 optional chaining, nullish coalescing, private field, class field, top-level await, async/await 변환(regenerator)과 같은 기능은 tsc가 ES5 수준으로 변환해주지 않는다. tsc는 타입 정보를 제거한 뒤 문법을 그대로 남겨두기 때문에, 브라우저가 해당 문법을 지원하지 않으면 코드가 그대로 SyntaxError를 내고 실행이 중단된다.

또한 tsc는 폴리필을 제공하지 않는다. Promise, Map/Set, Object.assign, Array.prototype.includes, fetch 같은 기능들은 tsc가 어떤 식으로든 보완해 주지 않는다. 브라우저가 지원하지 않는다면 역시 그대로 오류가 발생한다. 즉, tsc는 “트랜스파일러”이지만 Babel 같은 트랜스파일러와는 성격이 다르다. Babel/SWC는 ESNext 문법을 과거 환경으로 완전히 변환하거나, 필요한 경우 core-js와 regenerator를 통해 런타임 기능까지 폴리필할 수 있다. 반면 tsc는 타입 체크 및 타입 제거를 위한 도구이며 JS 다운레벨링 범위는 매우 제한적이다.

이런 이유로 tsc 출력물을 브라우저에서 바로 사용하는 것은 “가능할 때도 있지만, 신뢰할 수 있는 방식은 아니다.” 모던 브라우저를 대상으로 한 개발이라면 괜찮을 수 있지만, 다양한 브라우저나 구형 환경까지 고려한다면 Babel/SWC·Vite·Next.js 같은 빌드 시스템의 도움을 받는 것이 필수적이다. 이 빌드 도구들이 JS 문법 변환과 폴리필을 책임지고, tsc는 타입 체크만 하는 구조(tsc --noEmit)가 널리 쓰이는 이유다.


트랜스파일의 옵션은 뭐가 있을까

  • Babel

    가장 널리쓰이는 트랜스파일러이자 사실상의 표준이다. 다만, Babel은 기본적으로 JS 문법만 해석할 수 있으며, 수많은 TS 문법을 브라우저가 읽기 가능한 JS로 만드려면 수많은 규칙을 하나에 담은 Preset을 넣어줘야한다. 하지만 다양한 플러그인이 존재하기 때문에 React의 JSX, 최신 ECMAScript 문법, 심지어 TypeScript까지 모두 Babel이 처리할 수 있다.

    • plugin

      개별 문법에 대한 규칙으로 특정 문법(feature) 을 구버전에서도 동작하도록 바꿔줍니다.

    • Polyfill

      plugin은 문법 수준의 변환만 담당합니다. 즉, class, =>, optional chaining 같은 “언어 구문(syntax)”만 처리합니다. Polyfill 은 문법이 아니라, “런타임 API”를 보완하는 코드 최신 문법을 구형 문법으로 구현해놓은 것으로, Plugin에 Polyfill을 추가할 수 있다.

    • Preset

      Babel은 plugin의 변환 규칙에 따라 트랜스파일을 진행한다. 규칙을 세부적으로 다 할당 하긴 어려우니까 집합을 만들어 뒀는데, 이런 polyfill과 plugin 집합을 preset이라고 한다.

    Babel의 단점은 속도다. Babel은 JavaScript로 작성되어 있고, AST 변환을 전부 JS로 처리한다. 대규모 프로젝트에서는 이 AST 변환이 병목이 되며, 빌드 속도가 느려지는 가장 큰 원인 중 하나가 된다.

  • SWC

    SWC(Speedy Web Compiler)는 Babel의 느림을 정면으로 해결한 도구다. Babel과 거의 동일한 기능을 제공하지만, Rust로 구현되어 훨씬 빠르다. JS와 달리 Rust의 멀티스레드 병렬 변환을 지원하기 때문에 수십 배 빠르다. Next.js가 Babel 대신 SWC를 도입한 이유가 바로 이것이다.

    단점은 플러그인 생태계다. Babel은 오랜 세월 쌓인 수많은 preset과 transform plugin이 있지만, SWC만큼은 아니다. 하지만 Next에서 트랜스파일러를 Babel에서 SWC로 바꾼 것 만큼, 대부분의 기능은 별다른 설정 없이도 대체가 가능한 것으로 보인다.

  • 빌드도구

    요즘은 속도 빠른 esbuild나 vite 같은 번들러가 트랜스파일도 함께 해주니,
    tsc에게 타입체킹만을 위임하고 이런 번들링 도구를 써볼 수도 있다.


그나저나 Transpile은 어떻게 돌아가는걸까 ?

  1. Parsing

    개발당시 그저 파일 안에 텍스트로만 존재하던 코드들을 파싱한다.
    문자열 → 추상구문트리(AST)로 변환하는 과정을 갖는다.

  2. Trasnform

    트랜스파일러는 AST를 순회하면서 plugin, preset, polyfill에 따라 문법을 변환한다.
    예를 들어, 화살표 함수는 이렇게 바뀐다

    const sum = (a, b) => a + b; // Before
    var sum = function(a, b) { return a + b; }; // After
    

    이런 변환은 Babel의 preset이나 SWC의 transform 규칙에 정의되어 있다.
    Config에 정의된 규칙대로 트랜스파일러들은 변환한다.

  3. Code Generation

    수정된 AST를 다시 JS 코드 문자열로 출력한다.

    이제 브라우저가 실행할 수 있는 문법의 코드가 완성된다.

AST란,

자기만의 오퍼레이션을 위한 명령어들을 트리로 만들어둔 것이다. 브라우저가 JS 바이트 스트림을 읽고 AST를 만드는 것, 트랜스파일러가 코드를 읽고 AST를 만드는 것, 모두 일반 텍스트들을 읽고 앞으로 어떻게 행동할지를 기록해두는 것이다. ( 마치 기능명세서를 보고 TODO 리스트를 작성하는 우리처럼 )


References